import inspect
from functools import wraps
from itertools import zip_longest
from typing import Callable, Iterable
from types import FunctionType

"""
说明：
互相调用：正常
参数相关：
    默认值参数：
        如果没有设定默认值, 就默认为None
参数化函数：
    不管单个元素返回的是什么, 都传给第一个参数。
    
用例筛选：
    默认是除了"_"开头以外的所有函数, 其他功能待开发
"""
"""
疑问：
    1. 可以直接在把类给pytest运行吗？
        不行。
        它不接受含有构造函数的类，包括基类的构造函数。
        当基类有构造函数时，
        即使把当前类的构造函数删掉，也会存在基类的。
        而我们又必须要继承基类
"""


def params(data_s: Iterable):
    """参数化装饰器"""

    def d02(func):
        setattr(func, ParamsFuncs.params_, data_s)
        return func

    return d02


def filter_funcs(obj: type or dict):
    """筛选用例函数"""
    # obj参数兼容 实例对象 和 类字典对象
    if isinstance(obj, dict):
        obj_seq = obj.items()
    else:
        obj_seq = type(obj).__dict__.items()
    return (
        (key, value)
        for key, value in obj_seq
        if not (key.startswith("_") or  # 私有变量
                not callable(value)  # 不可调用的
                )
    )


def my_partial(func, *args_1, **kwargs_1):
    """
    自定义的partial, 用于解决手动实例化时缺少self的错误
    可以叠加使用, 支持可变参数
    """

    @wraps(func)
    def run(*args_2, **kwargs_2):
        args_all = args_1 + args_2
        kwargs_1.update(kwargs_2)
        return func(*args_all, **kwargs_1)

    return run


class ParamsFuncs:
    """参数化的实现"""
    params_ = "iterable_"

    def __init__(self, attrs):
        # 目前会固定把可迭代对象的元素, 都传给第一个参数（不是self）
        self.attrs_ = dict(self.main(attrs))

    def main(self, attrs):
        """
        循环attrs
        找到符合的, 先循环生成再返回
        不符合的, 直接返回
        """
        for k, v in attrs.items():
            if callable(v) and hasattr(v, self.params_):
                yield from self.gen_func(k, v, getattr(v, self.params_))
            else:
                yield k, v

    def gen_func(self, name, func, iterable_):
        """生成参数化函数并返回"""
        # 获取第二个参数名称
        arg_name = inspect.getfullargspec(func).args[1]
        # 先返回原函数,
        # 再返回其他函数, 其他函数名称从2开始递增
        for i, v in enumerate(iterable_, 1):
            func_ = my_partial(func, **{arg_name: v})
            if i == 1:
                yield name, func_
                continue
            # 修改函数名称, 并作为属性名称返回
            func_.__name__ = f"{name}{i}"
            yield func_.__name__, func_


class Meta1(type):
    """注入修改后的参数化函数"""

    def __new__(mcs, name, bases, attrs, **kwargs):
        # 先忽略无关的基类
        if f"_{name}__ignore" in attrs.keys():
            __args = name, bases, attrs
            return type.__new__(mcs, *__args, **kwargs)
        # 先判断函数参数的合法性
        mcs.__check_args(attrs)
        # 处理参数化函数
        __args = (name, bases, ParamsFuncs(attrs).attrs_)
        return type.__new__(mcs, *__args, **kwargs)

    @classmethod
    def __check_args(mcs, attrs):
        # 判断 参数数量 是否等于 默认参数.
        # 1. 先减1, 减去去self参数,
        # 2. 如果是参数化的函数, 再减1
        # 3. 最后：先判断默认参数是否为真, 最后再比较数量
        for _, func in filter_funcs(attrs):
            inspect_func = inspect.getfullargspec(func)
            args_ = inspect_func.args
            len_args = len(args_)
            len_args -= 1
            if hasattr(func, ParamsFuncs.params_):
                len_args -= 1
            if len_args > 0:
                # 1.如果defaults为空, 则替换为空元组。
                # 使用长板并行迭代, 数量少的就补充None。正好合适就不补充
                defaults = inspect_func.defaults or ()
                func.__defaults__ = tuple((
                    b for a, b
                    in zip_longest(args_, defaults)))


class CasesObj:
    def __init__(self, obj):
        self.obj = obj

    def __iter__(self):
        return self

    def __next__(self) -> FunctionType:
        # 用于声明返回值, 方便后续的属性提示.
        # (__iter__不能单个元素, 而__next__可以)
        return next(self.obj)

    def __str__(self):
        cases = [_.__name__ for _ in self.obj]
        return f"名称：{cases} \n数量: {len(cases)}"

    def __call__(self, *args, **kwargs):
        """对ide声明"""
        pass


class CollectCases(metaclass=Meta1):
    """收集用例"""
    __ignore = "meta"

    # 1.实例化时能正常的运行
    #   打印实例时, 会输出用例
    #   迭代实例时, 会迭代用例
    # 2.不实例化, 使用类方法能返回所有用例
    #  （作用：不会在被pytest导入时就实例化,
    #    而是实际运行用例时, 才运行实例化的代码）
    #   注意: 不能在打印的同时又迭代, 这样会导致循环两次, 而这种方式不能重复实例化

    @classmethod
    def cases(cls):
        """收集用例的启动函数"""
        _1 = cls.__repl_init()  # 替换init,并取出
        _2 = cls()  # 预先实例化
        _3 = cls.__bind_self  # 过滤函数, 并绑定self参数
        _4 = CasesObj  # 返回可以打印的 迭代器对象
        return _4(_3(_2, _1))

    @classmethod
    def __repl_init(cls):
        def init_(*args, **kwargs): pass

        # 原来的init与空init的交换, 再改名
        if not hasattr(cls, "_init"):
            cls.__init__, cls._init = init_, cls.__init__
            cls._init.__name__ = "init"
        return cls._init

    @classmethod
    def run_all(cls):
        # 实例对象不能调用, 可能会重复实例化
        for case in cls.cases():
            case()

    def __bind_self(self, init_: Callable = None):
        """生成用例函数"""

        # 绑定self参数、控制顺序
        # 最初传入的必须是self, 而不能是cls。因为需要给其他函数绑定self
        attrs_ = filter_funcs(self)

        # 先返回合并的函数：构造函数与第一个函数
        if init_:
            func_1 = next(attrs_)[1]

            def merge(self_):
                init_(self_)
                func_1(self_)
                pass

            merge.__name__ = func_1.__name__
            yield my_partial(merge, self)
        # 再返回其他
        for key, value in attrs_:
            yield my_partial(value, self)

    def __str__(self):
        cases = [i.__name__ for i in self.__bind_self()]
        msg = f"{cases} \n数量: {len(cases)}"
        return msg

    def __len__(self):
        return sum(1 for _ in self.__bind_self())
